﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Xml;
using CSharpExt.Util;
using log4net;

namespace CSharpExt.Xml
{

    // ======================================================================
    // DocumentResolver
    // ======================================================================

    /// <summary>
    /// La seguente classe permette di semplificare la scrittura di un file di configurazione aggiungendo 
    /// le seguenti funzionalita':
    /// 
    /// - la possibilita' di avere una configurazione parametrica, ed utilizzare dei file esterni da 
    ///   poter includere e contenenti le definizioni dei parametri
    /// 
    /// Puo' essere utilizzata in tre modi:
    /// 
    /// - riceve in ingresso il path di un file di configurazione e ritorna il path del file con  tutti i
    ///   parametri risolti
    /// - riceve in ingresso il path di un file di configurazione e ritorna il DOM con tutti i parametri 
    ///   risolti
    /// - riceve in ingresso il DOM di una configurazione (e almeno il path della directory dove cercare
    ///   eventuali file da includere espressi come path relativo) e ritorna lo stesso DOM con tutti i 
    ///   parametri risolti
    /// 
    /// Configurazione Parametrica
    /// --------------------------
    /// 
    /// E' possibile parametrizzare la configurazione dei filtri, in modo da centralizzare alcune definizioni.
    /// L'idea e' la seguente: si puo' scrivere un file di configurazione avente la seguente struttura:
    /// 
    ///     <?xml version="1.0" encoding="utf-8"?>
    ///     <Profiles>
    ///         <Properties include="commons.xml"/>
    ///         <Properties include="/local/specific.xml">
    ///             <Property name="home.dir"  value="/Sole24"/>
    ///             <Property name="home.dump" value="${home.dir}/Dump"/>
    ///             <Property name="type"      value="mixful"/>
    ///             <Property name="check.timeout" value="10000"/>
    ///         </Properties>
    /// 
    ///         <Profile name="MixerFull" LoadOnStartup="true" RestartOnError="false" AutoShutdownFiltername="" EnablePerfmon="false">
    ///             <Filters>
    ///                 <Filter type="StreamToBufferFilter" name="MetaData">
    ///                     <Configuration>
    ///                         <Field name="filePath" value="${home.dump}/${Filter.name}.${type}.txt"/>
    ///                         <Field name="capacity" value="0kb/s"/>
    ///                         <Field name="checkTimeout" value="${check.timeout}"/>
    ///                     </Configuration>
    ///                 </Configuration>
    ///             </Filter>
    ///             ...
    ///         </Profile>
    ///     </Profiles>
    /// 
    /// E' possibile indicare uno o piu' tag <Properties>, che puo' avere una delle seguenti forme:
    /// 
    ///     <Properties include="includePath"/>
    /// 
    /// in cui 'includePath' e' il path di un file XML che deve essere incluso.  Il path puo' essere 
    /// assoluto o relativo alla directory contenente il file di configurazione. Il file da includere 
    /// deve avere la forma:
    /// 
    ///     <?xml version="1.0" encoding="utf-8"?>
    ///     <Properties>
    ///         <Property name="home.dump" value="/Sole24/Dump"/>
    ///         <Property name="type"      value="mixful"/>
    ///         <Property name="check.timeout" value="10000"/>
    ///     </Properties>
    /// 
    /// Oppure, e' possibile indicare esplicitamente l'elenco delle definizioni  (ovviamente ha la stessa 
    /// struttura del file da includere)
    /// 
    ///     <Properties>
    ///         <Property name="home.dump" value="/Sole24/Dump"/>
    ///         <Property name="type"      value="mixful"/>
    ///         <Property name="check.timeout" value="10000"/>
    ///     </Properties>
    /// 
    /// E' possibile anche utilizzare una configurazione ibrida:
    /// 
    ///     <Properties include="/local/specific.xml">
    ///         <Property name="home.dump" value="/Sole24/Dump"/>
    ///         <Property name="type"      value="mixful"/>
    ///         <Property name="check.timeout" value="10000"/>
    ///     </Properties>
    /// 
    /// in cui si indica sia il file da includere, che delle nuove definizioni.
    /// 
    /// Esistono alcune property che vengono inizializzate durante il processo di risoluzione delle variabili:
    /// 
    /// - 'Home.directory': path completo della directory contenente il file di configurazione
    /// - 'Profile.name': usabile all'interno di un profilo, contiene il valore di "Profile/@name" ("MixerFull")
    /// - 'Filter.name':  usabile all'interno di un filtro, contiene il valore di "Filter/@name"   ("MetaData")
    /// - 'Filter.type':  usabile all'interno di un filtro, contiene il valore di "Filter/@type"   ("StreamToBufferFilter")
    /// 
    /// Nota: le property a loro volta possono essere parametrizzate
    /// Nota: attenzione alle definizioni ricorsive
    /// Nota: attenzione alla sintassi della variabile, che deve essere:  ${nomeVar}
    /// 
    /// 
    /// Deduzione del nome del profilo
    /// ------------------------------
    /// Se il file di configurazione contiene un solo profile (come capita spesso nei casi di test), risulta
    /// scomodo dover mantenere allineato il nome del file con il nome del profilo. In questo caso la soluzione
    /// e' semplice: non si assegna nessun nome al profilo ("Profile name=''") e il resolver lo sostituira' 
    /// con il nome del file (escludendo la directory e l'estensione).
    /// 
    /// E' possibile avere piu' profili senza nome, in questo caso i profili successivi avranno come nome, il nome
    /// del file seguito da un contatore
    /// </summary>
    public static class DocumentResolver
    {
        #region Logging

        private static ILog _logger = LogManager.GetLogger(typeof(DocumentResolver));

        #endregion

        #region Interface

        public static XmlDocument LoadAndResolve(String filePath)
        {
            return LoadAndResolve(filePath, null);
        }

        public static XmlDocument ParseAndResolve(String homeDirectory, String xml)
        {
            return ParseAndResolve(homeDirectory, xml, null);
        }


        /// <summary>
        /// Legge il file, risolve tutte le parti parametrizzate, e ritorna il DOM
        /// </summary>
        /// <param name="filePath">path del file di configurazione</param>
        /// <returns>il DOM con tutte le parti risolte</returns>
        public static XmlDocument LoadAndResolve(String filePath, Properties props)
        {
            FileInfo fInfo = new FileInfo(filePath);

            props = props ?? new Properties();

            // rende assoluto il path passato in 'filePath'
            filePath = fInfo.FullName;

            XmlDocument doc = new XmlDocument();

            XmlReader xmlr = XmlReader.Create(filePath, new XmlReaderSettings()
            {
                DtdProcessing = DtdProcessing.Ignore, 
                CloseInput = true
            });

            try
            {
                // legge la configurazione
                doc.Load(xmlr);
            }
            finally
            {
                xmlr.Close();
            }

            // risolve le variabili, e l'abilitazione dei performance counters
            ResolveVariables(filePath, doc.DocumentElement, props);

            return doc;
        }

        public static XmlDocument ParseAndResolve(String homeDirectory, String xml, Properties props)
        {
            String filePath = Path.Combine(homeDirectory, "dummy");

            props = props ?? new Properties();

            // legge la configurazione
            XmlReader xmlr = XmlReader.Create(new StringReader(xml), 
                                              new XmlReaderSettings()
                                              {
                                                  DtdProcessing = DtdProcessing.Ignore, 
                                                  XmlResolver = new FileXmlResolver(homeDirectory)
                                              });
            XmlDocument doc = new XmlDocument();
            doc.Load(xmlr);

            // risolve le variabili, e l'abilitazione dei performance counters
            ResolveVariables(filePath, doc.DocumentElement, props);

            return doc;
        }

        /// <summary>
        /// Risolve tutte le parti parametriche della configurazione
        /// </summary>
        /// <param name="profileName">Nome del profilo. Usato se non indicato esplicitamente</param>
        /// <param name="homeDirectory">Directory di riferimento per i path relativi dei file da includere</param>
        /// <param name="config">DOM rappresentante la configurazione corrente</param>
        /// <param name="enablePerfmon">se deve abilitare i Performance Counter globalmente</param>
        /// <returns>'true' se sono state fatte delle risoluzioni, 'false' altrimenti</returns>
        private static bool ResolveVariables(String filePath, XmlElement config, Properties props)
        {
            bool createUpdated = false;

            // controlla che 'homeDirectory' non sia null
            if (String.IsNullOrEmpty(filePath))
            {
                filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "dummy");
            }

            FileInfo fInfo = new FileInfo(filePath);
            String fileName = dropExt(fInfo.Name);
            String homeDirectory = fInfo.DirectoryName;

            // ricupera le properties
            {
                props.Add("Home.directory", homeDirectory);
                props.Add("File.name", fileName);

                XmlNodeList propsList = config.SelectNodes("./Properties");
                for (int i = 0; i < propsList.Count; ++i)
                {
                    XmlElement propsNode = (XmlElement)propsList[i];

                    readProperties(props, propsNode);

                    propsNode.ParentNode.RemoveChild(propsNode);
                }

                // se sono state trovate delle property, allora scandisce il
                // DOM per applicare le risoluzioni
                if (props.Count > 0)
                {
                    replaceProperties(config, props);
                    createUpdated = true;
                }
            }

            // se non ci sono state modifiche, allora il DOM non e' stato modificato
            return (createUpdated);
        }

        #endregion

        #region Implementation

        class FileXmlResolver : XmlResolver
        {
            private String _homeDirectory;

            internal FileXmlResolver(String homeDirectory)
            {
                _homeDirectory = homeDirectory;
            }

            public override Object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
            {
                throw new NotImplementedException();
            }

            public override ICredentials Credentials
            {
                set { }
            }
        }

        private static String dropExt(String n)
        {
            int pos = n.LastIndexOf('.');
            if (pos == -1)
                return n;
            else
                return n.Substring(0, pos);
        }

        private static void readProperties(Dictionary<String, String> props, XmlElement propsNode)
        {
            String includeFile = propsNode.GetAttribute("include");
            if (!String.IsNullOrEmpty(includeFile))
            {
                if (!Path.IsPathRooted(includeFile))
                {
                    String homeDirectory = props["Home.directory"];
                    includeFile = Path.Combine(homeDirectory, includeFile);
                }

                readProperties(props, includeFile);
            }

            XmlNodeList properties = propsNode.SelectNodes("Property");
            if (properties == null)
                return;

            for (int i = 0, n = properties.Count; i < n; ++i)
            {
                XmlElement property = (XmlElement)properties.Item(i);
                String name = property.GetAttribute("name").Trim();
                String value = property.GetAttribute("value").Trim();

                if (String.IsNullOrEmpty(name))
                    continue;

                if (String.IsNullOrEmpty(value))
                    value = String.Empty;

                props.Remove(name);
                props.Add(name, value);
            }
        }

        private static void readProperties(Dictionary<String, String> props, String includeFile)
        {
            if (!File.Exists(includeFile))
            {
                _logger.WarnFormat("Unable to include file '{0}'", includeFile);
                return;
            }

            XmlDocument config = new XmlDocument();
            config.Load(includeFile);
            readProperties(props, config.DocumentElement);
        }

        private static void replaceProperties(XmlNode node, Dictionary<String, String> props)
        {
            switch (node.NodeType)
            {
                case XmlNodeType.Attribute:
                    node.Value = replaceProperties(node.Value, props);
                    break;

                case XmlNodeType.Element:
                    XmlNamedNodeMap attributes = node.Attributes;


                    for (int i = 0, n = attributes.Count; i < n; ++i)
                    {
                        replaceProperties(attributes.Item(i), props);
                    }

                    XmlNodeList nl = node.ChildNodes;
                    for (int index = 0; index < nl.Count; ++index)
                    {
                        XmlNode child = nl.Item(index);

                        replaceProperties(child, props);
                    }
                    break;

                case XmlNodeType.Text:
                case XmlNodeType.CDATA:
                    node.Value = replaceProperties(node.Value, props);
                    break;
            }
        }

        private static String replaceProperties(String s, Dictionary<String, String> props)
        {
            int pos = s.IndexOf("${");
            int end = s.IndexOf("}", pos + 1);
            while (pos != -1 && end != -1)
            {
                String prop = s.Substring(pos, end - pos + 1);
                String key = prop.Substring(2, prop.Length - 3).Trim();
                String value;
                if (props.TryGetValue(key, out value))
                {
                    s = s.Replace(prop, value);
                }
                else
                {
                    throw new XmlException("Missing value for " + prop);
                }

                pos = s.IndexOf("${", pos);
                end = s.IndexOf("}", pos + 2);
            }
            return s;
        }

        #endregion
    }
}
